Skip to main content

Infinite Queries


渲染可以将额外的数据添加到现有数据集中或进行无限滚动的列表也是一种非常常见的UI模式。TanStack Query支持一个有用的useQuery版本,称为useInfiniteQuery,用于查询这些类型的列表。

在使用useInfiniteQuery时,您会注意到一些不同之处:

  • data现在是一个包含无限查询数据的对象:
  • data.pages数组包含获取的页面
  • data.pageParams数组包含用于获取页面的页面参数
  • 现在可用fetchNextPagefetchPreviousPage函数
  • 可用于确定是否有更多数据可加载以及获取该信息的getNextPageParamgetPreviousPageParam选项。此信息作为额外参数提供给查询函数(在调用fetchNextPagefetchPreviousPage函数时可以选择覆盖它)
  • 如果getNextPageParam返回一个除undefined以外的值,则现在有一个hasNextPage布尔值为true
  • 如果getPreviousPageParam返回一个除undefined以外的值,则现在有一个hasPreviousPage布尔值为true
  • 现在可用isFetchingNextPageisFetchingPreviousPage布尔值来区分后台刷新状态和加载更多状态

注意:当在查询中使用initialDataselect等选项时,请确保在重组数据时仍包括data.pages和data.pageParams属性,否则您的更改将被查询在其返回中覆盖! Note: When using options like initialData or select in your query, make sure that when you restructure your data that it still includes data.pages and data.pageParams properties, otherwise your changes will be overwritten by the query in its return!

Example

假设我们有一个基于游标索引的API,根据每次3个项目返回一页的项目,并附带一个游标,可用于获取下一组项目:

fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }

有了这些信息,我们可以通过以下方式创建一个"加载更多"的UI:

  • 默认情况下等待useInfiniteQuery请求第一组数据
  • getNextPageParam中返回下一个查询的信息
  • 调用fetchNextPage函数

注意:非常重要的是,不要给fetchNextPage传递参数,除非您希望用它们覆盖getNextPageParam函数返回的pageParam数据。例如,不要这样做:<button onClick={fetchNextPage} />,因为这会将onClick事件发送给fetchNextPage函数。

import { useInfiniteQuery } from '@tanstack/react-query'

function Projects() {
const fetchProjects = async ({ pageParam = 0 }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}

const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

return status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}

当需要重新获取无限查询时会发生什么?

当一个无限查询变得过时并需要重新获取时,每个分组都会按顺序进行获取,从第一个分组开始。这样可以确保即使底层数据发生变化,我们也不会使用过时的游标,从而避免出现重复数据或跳过记录。如果无限查询的结果从查询缓存中被删除,分页将重新从初始状态开始,只请求初始分组。

refetchPage

如果您只想主动重新获取所有页面中的子集,可以将refetchPage函数传递给useInfiniteQuery返回的refetch

const { refetch } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

// 只重新获取第一页
refetch({ refetchPage: (page, index) => index === 0 })

您还可以将此函数作为第二个参数(queryFilters)的一部分传递给queryClient.refetchQueriesqueryClient.invalidateQueriesqueryClient.resetQueries

签名: refetchPage: (page: TData, index: number, allPages: TData[]) => boolean 对于每个页面,将执行此函数,并且仅重新获取函数返回true的页面。

如果您需要向查询函数传递自定义信息怎么办?

默认情况下,从getNextPageParam返回的变量将被传递给查询函数,但在某些情况下,您可能希望覆盖这个默认行为。您可以通过fetchNextPage函数传递自定义变量,这将覆盖默认的变量传递方式,例如:

function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)

const {
status,
data,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})

// Pass your own page param
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}

如果我想要实现一个双向无限列表怎么办?

双向列表可以通过使用getPreviousPageParamfetchPreviousPagehasPreviousPageisFetchingPreviousPage属性和函数来实现。

useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})

如果我想要以相反的顺序显示页面怎么办?

有时您可能希望以相反的顺序显示页面。如果是这种情况,您可以使用select选项:

useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})

如果我想要手动更新无限查询怎么办?

手动删除第一页:

queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))

如果我想要手动从单个页面中删除一个值怎么办?

const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []

queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))

确保保持页面(pages)和页面参数(pageParams)的相同数据结构。